/*
 * GeoVISTA Center (Penn State, Dept. of Geography)
 * Copyright (c), 2003-2011,  Jin Chen and Alan M. MacEachren, GeoVISTA Center, Penn State University
 * All Rights Researved.
 *
 * This library is developed and distributed for free usage,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * You can redistribute and/or modify it under the terms of the
 * Simplified BSD License
 *
 *
 *
 *
 *
 *
 *
 *
 * @author: jin Chen
 * @date: Sep 29, 2003$
 * @version: 1.0
 */
package edu.psu.geovista.app.pcp.plot.services;

import cern.colt.list.DoubleArrayList;
import cern.jet.stat.Descriptive;
import edu.psu.geovista.app.pcp.toolkits.Math.JMath;
import edu.psu.geovista.app.pcp.toolkits.group.DataIDGroup;
import edu.psu.geovista.app.pcp.toolkits.group.DataIDSet;
import edu.psu.geovista.app.plot.axis.AxisList;
import edu.psu.geovista.app.plot.axis.BoxPlot;
import edu.psu.geovista.app.plot.axis.YAxis;
import edu.psu.geovista.data.model.AppDataModel;

import java.awt.geom.Line2D;
import java.util.*;

public class CommonService {
    public static final boolean DEBUG=false;
    public static final String Service_Animation="sanm";

    protected AppDataModel dataModel;

    Hashtable components=new Hashtable();

    public CommonService(AppDataModel dataModel) {
        this.dataModel = dataModel;
    }
    /*******************************************************************************************************
     *                state median summary
     *******************************************************************************************************/
    /**
     * deprecated, use getObservationSummaryAsDataModel()
     * @param data
     * @return
     */
    public static edu.psu.geovista.data.model.AppDataModel getObservationSummary(edu.psu.geovista.data.model.AppDataModel data) {
       AppDataModel stModel=edu.psu.geovista.data.model.DefaultPlotModel.createDefaultPlotModel();//state median summary Model
       Hashtable rowGroupSum=new Hashtable();//row (observation) summary. Contain rowName:rowIDs(HashSet) pair
       //create observation group
       for (int i=0;i<data.getRowCount() ;i++){
           String rowName=data.getRowName(i);
           if(rowName==null||rowName.length() ==0){
               //??? in case data don't have row name need find a way to info user
               continue;
           }
           HashSet ids=(HashSet) rowGroupSum.get(rowName);
           if(ids==null){
               ids=new HashSet();
               rowGroupSum.put(rowName,ids);
           }
           ids.add(new Integer(i));

       }

       //create observation median record
       Enumeration keys=rowGroupSum.keys() ;
       while (keys.hasMoreElements()) {
           String obName = (String) keys.nextElement(); //observation name
           HashSet obIDs=(HashSet) rowGroupSum.get(obName);// id of records that belong to the observation
           float[] stateMedians=generateMedianRecord(data,obIDs);//a record contain the median values for the state
           stModel.addRow(stateMedians,obName); //set record value

       }
       List columnNames=data.getColumnNameList() ;
       stModel.setColumns(data.getColumns()) ;//set the column names for (numeric)values only. For classify purpose
       return stModel;
    }
    /*public static AppDataModel getObservationSummaryAsDataModel(DefaultPlotModel data) {
        DefaultPlotModel stModel=new DefaultPlotModel();//state median summary Model
        DataIDGroup grp=getObservationSummaryAsGroup(data);
        Enumeration keys=grp.keys() ;
       while (keys.hasMoreElements()) {
           String id = (String) keys.nextElement(); //observation name
           DataIDSet obIDs=grp.getDataIDSet(id) ;// id of records that belong to the observation
           float[] stateMedians=generateMedianRecord(data,obIDs.getIdAsSet() );//a record contain the median values for the state
           stModel.addRecord(stateMedians,obIDs.getName() );

       }
       //List columnNames=data.getColumnNameList() ;
       stModel.setColumns(data.getColumns()) ;//for classify purpose
       return stModel;
    }*/
    /**
     * Don't go through the process of generate DataIDGroup.
     * @param data
     * @param grp
     * @return
     */
    public static edu.psu.geovista.data.model.AppDataModel getObservationSummaryAsDataModel(edu.psu.geovista.data.model.AppDataModel data,DataIDGroup grp) {
        AppDataModel stModel=edu.psu.geovista.data.model.DefaultPlotModel.createDefaultPlotModel();//state median summary Model

         /**
          * create index-sort list is to make sure record index in dataModel map to index of each IndexSet in DataIDGroup
          * It is for fireEvent upon selection: when you select a line on plot under Mode_observationSummary mode,
          * you get id of data (in dataModel) which is repsented by the line, if you want to know what records are summarized into the line
          * you must pass the id  to DataIDGroup and get DataIDSet and thus get the ids.
          */
         ArrayList grpList=grp.getIndexSortList() ;// 
        for (Iterator iterator = grpList.iterator(); iterator.hasNext();) {
            Object o= iterator.next();
            DataIDSet obIDs = (DataIDSet)o;  //
            String obname=obIDs.getId() ;
            float[] stateMedians=generateMedianRecord(data,obIDs.getIdAsSet() );

            stModel.addRow(stateMedians,obIDs.getId() ); //obIDs.getId() return record name
        }
        /*Enumeration keys=grp.keys() ;
       while (keys.hasMoreElements()) {
           String id = (String) keys.nextElement(); //observation name
           DataIDSet obIDs=grp.getDataIDSet(id) ;// id of records that belong to the observation
           float[] stateMedians=generateMedianRecord(data,obIDs.getIdAsSet() );//a record contain the median values for the state

           stModel.addRecord(stateMedians,obIDs.getName() );

       }*/
       //List columnNames=data.getColumnNameList() ;
       stModel.setColumns(data.getColumns()) ;//for classify purpose
       return stModel;
    }
    public static DataIDGroup getObservationSummaryAsGroup(edu.psu.geovista.data.model.AppDataModel data, String variable) {
        DataIDGroup grp=new DataIDGroup();
        //Has protential to do any summary based on any column as long as provide the column data as list based on which the summary is going to make
        List sumCol=null;//a list of name of all observation(row) data
        boolean useRowName=false;
        if (variable==null){//by default, use observation name as summary variable
                sumCol = data.getRowNames();
                useRowName=true;
        }
        else{
            Object[] col=data.getNonNumericColumnData(variable);
            if(col==null||col.length ==0||!(col instanceof String[])){
                  sumCol = data.getRowNames();
                  useRowName=true;
            }
            else{
                 sumCol=new ArrayList();
                for (int i=0;i<col.length ;i++){

                    sumCol.add((String)col[i]);

                }
            }
        }
       /* if (variable==null || ) {
            sumCol = data.getRowNames();
        } else {
        }*/
        if(sumCol==null || sumCol.size()==0){
            throw new IllegalArgumentException("Given data don't have observation name, unable to do observation summary");
        }
        for (int i=0;i<data.getRowCount() ;i++){
           String obsrName=(String) sumCol.get(i) ; //observation name
           if(obsrName==null||obsrName.length() ==0){
               //??? in case data don't have row name need find a way to info user
               continue;
           }
           // 
           // 
           // 
           DataIDSet ids= (DataIDSet) grp.getIndexSet(obsrName);
           if(ids==null){
               ids=new DataIDSet();
               ids.setId(obsrName); //The ID of the IndexSet. e.g.: A state name
               // 
               ids.setIndex(grp.size() -1);
               grp.add(ids); //
               /*if(rowName.equals("Arizona") ){
                   System.out.println("Arizona id:"+i);
               }*/
           }
           ids.add(i);

       }
       return grp;
    }
    /**
     * Given data and record ids, for each column generate median value from given records with ids
     * @param data
     * @param ids
     * @return     A float array, each element is median of the column
     */
    public  static float[] generateMedianRecord(edu.psu.geovista.data.model.AppDataModel data,Set ids){
        int numOfColumn=data.getColumnCount() ;

        float[] medians=new float[numOfColumn];
        if(ids==null||ids.size() ==0){
            Arrays.fill(medians,Float.NaN);
            return medians;
        }
        //extract group data
        int numOfRow=ids.size() ;
        //float[][] grpData=new float[numOfRow][numOfColumn];
        AppDataModel grpData=edu.psu.geovista.data.model.DefaultPlotModel.createDefaultPlotModel();
        Object[] IDs=ids.toArray() ;
        for (int i=0;i<numOfRow ;i++){//for each row
             int id=((Integer)IDs[i]).intValue() ;//row id
             float[] record=data.getRowValueAsFloat(id);
             grpData.addRow(record);
             //System.arraycopy(record,0,grpData[i],0,record.length );
        }

        //calculate median for each column
        for (int i=0;i<numOfColumn;i++){
            float[] colData=grpData.getColumnValueAsFloat(i);
            medians[i]=JMath.calculateMedian(colData);

        }
        return medians;
    }

    /*******************************************************************************************************
     *                show range
     *******************************************************************************************************/
    public  int[]  getRecordsByShowRange(AxisList yaxisList){
         //AppDataModel dataModel=this.ui.getPlot().getDataModel();
         //AxisList yaxisList=this.ui.getPlot().getYaxisList() ;
         int numOfRecord=dataModel.getRowCount() ;
         int numOfAxis=yaxisList.size() ;
         //int numOfAxis=dataModel.getNumDimensions() ;

         int ids[] = new int[numOfRecord];
         int count = 0;

         for (int i = 0; i < numOfRecord; i++) { //i: record(row) index
              float[] record = (float[]) dataModel.getRowValueAsFloat(i);
              int actId=0;
              boolean validRecord=true;
              for (int j=0;j<numOfAxis;j++){
                  YAxis axis = yaxisList.getAxis(j);

                  //See if a record in showRange of a axis
                  if (/*axis.isVisible() && */
                      (!axis.isInShowRange(record[axis.getDataIndex() ]) )){
                      //axis.isInShowRange(record[j]) is NOT correct when re-order axis
                       validRecord=false;
                      //Debug: if decrease max of show range for POP1990, record 2389 is the first invalid one
                       break;
                   }
              }
              if (validRecord){
                 ids[count++] = i;
              }
         }
         if (count > 0) {
            int newids[] = new int[count];
            System.arraycopy(ids,0,newids,0,count);
            if(DEBUG)
                System.out.println("showrange ids:"+newids.length );
            return newids;
        }
        else{
              if(DEBUG)
                System.out.println("showrange ids:"+0 );
             return new int[0];
         }

    }
    /*******************************************************************************************************
     *                brush (select)
     *******************************************************************************************************/
     /**
      * For Selection
      * Assume brushing happen between 2 axis
      * @param axFrm  (display) axis on left
      * @param axTo    (display)axis on right
      * @param line  brushing line
      * @return       record ID of all record that is selected ( intersected by brushing line)
      */
     public  HashSet getBrushedRecord(Set domain, YAxis axFrm, YAxis axTo, Line2D.Float line ){
         //AppDataModel dataModel=this.ui.getPlot().getDataModel();
         /*AxisList yaxisList=this.ui.getPlot().getYaxisList() ;
         YAxis axFrm = yaxisList.getAxis(from);
         YAxis axTo=yaxisList.getAxis(to);
*/
         int numOfRecord=0;
         boolean useWholeData=false;
         Object[] IDs=null;
         if (domain==null) {
             numOfRecord = dataModel.getRowCount();
             useWholeData=true;
         } else {
             numOfRecord=domain.size() ;
             IDs=domain.toArray() ;
             useWholeData=false;
         }
         boolean validRecord=false;
         //int ids[] = new int[numOfRecord];
         HashSet ids=new HashSet();
         int count = 0;


         int x1=axFrm.getLocation().x ;
         int x2=axTo.getLocation().x ;
         for (int k = 0; k < numOfRecord; k++) { //i: record(row) index
              /*if(i==2389){
                  System.out.println("2389");
              }*/
              int id;
              if(useWholeData){
                  id=k;
              }
              else{
                  id=((Integer)IDs[k]).intValue() ;
              }
              float[] record = (float[]) dataModel.getRowValueAsFloat(id);
              float rawy1=record[axFrm.getDataIndex() ];
              float rawy2=record[axTo.getDataIndex() ];

              if(Float.isNaN(rawy1)||Float.isNaN(rawy2) ) continue;//won't select invalid data
              int y1=axFrm.transform(rawy1) ;
              int y2=axTo.transform(rawy2) ;

              validRecord=line.intersectsLine(x1,y1,x2,y2);


              if (validRecord){
                 //ids[count++] = i;
                  ids.add(new Integer(id)) ;
              }
         }
         return ids;

     }


    /*******************************************************************************************************
     *                boxplot
     *******************************************************************************************************/
    public void updateBoxplotForAllAxis(AxisList yaxisList) {
        Iterator iter=yaxisList.iterator() ;
        while(iter.hasNext()){
          YAxis yaxis = (YAxis)iter.next();
          int dataIndex=yaxis.getDataIndex() ;
          //BoxPlot bp=yaxis.getBoxplot() ;
          float[] column=dataModel.getColumnValueAsFloat(dataIndex);
          BoxPlot bp=new BoxPlot(column);
          yaxis.setBoxplot(bp);
          //bp.setRawData(column);
       }

    }
    /*******************************************************************************************************
     *                correlation
     *******************************************************************************************************/
     /**
     * Given a primary axis name and  a array of axis names, calculate the correlation btw primary axis and each of the axis in the array
     * @param axname
     * @param axnames
     * @return
     */
    public  List<DfCorrelationResult> getCorrelations(String axname,String []axnames){
          return getCorrelations(axname,axnames,this.getDataModel() );
    }
      /**
     * Given a primary axis name and  a array of axis names, calculate the correlation btw primary axis and each of the axis in the array
     * @param paxname   primary axis name.
     * @param axnames
     * @param dataModel   contain data for each axis
     * @return
     */
    public static List<DfCorrelationResult> getCorrelations(String paxname, String[] axnames, AppDataModel dataModel) {
        if(dataModel==null)return null;

        ArrayList list=new ArrayList(axnames.length );
        for (int i=0;i<axnames.length ;i++){
           String name=axnames[i];
            int index1 = dataModel.getColumnIndexByName(paxname);
            int index2 = dataModel.getColumnIndexByName(name);
            double[] v1 = dataModel.getColumnValueAsDouble(index1);
            double[] v2 = dataModel.getColumnValueAsDouble(index2);

            CorrelationResult corr = getCorrelation(v1,v2);
            double corrvalue = corr.getValue();
            CorrelationResult cr=new DfCorrelationResult();
            cr.setPrimaryVariable(paxname);
            cr.setSecondVariable(name);
            cr.setValue(corrvalue);
            list.add(cr);
        }
        return list;
    }

   
     public  CorrelationResult getCorrelation(YAxis ax1,YAxis ax2){
            String n1 = ax1.getName();
            String n2 = ax2.getName();
            edu.psu.geovista.data.model.AppDataModel dataModel = (edu.psu.geovista.data.model.AppDataModel) this.getDataModel();
            int index1 = dataModel.getColumnIndexByName(n1);
            int index2 = dataModel.getColumnIndexByName(n2);
            double[] v1 = dataModel.getColumnValueAsDouble(index1);
            double[] v2 = dataModel.getColumnValueAsDouble(index2);

        return getCorrelation(v1, v2);

    }

    public static CorrelationResult getCorrelation(double[] v1, double[] v2) {
        DoubleArrayList[] valids = getValidData(v1,v2);

        /*double[] validv1=getValidData(v1);
        double[] validv2=getValidData(v2);

        DoubleArrayList d1=new DoubleArrayList(validv1);
        //d1.trimToSize();
        DoubleArrayList d2=new DoubleArrayList(validv2);*/
        //d2.trimToSize();
        DoubleArrayList d1=valids[0];
        DoubleArrayList d2=valids[1];
        double s1= getStandDeviation(d1);
        double s2=getStandDeviation(d2);
        double cr =  Descriptive.correlation(d1, s1, d2, s2);
        //System.out.println("computed cr");

        //double cr = Correlation.getCorrelationResult(validv1,validv2);
        int numOfInvalid=v1.length -d1.size();// + v2.length -validv2.length ;

        /*if(Double.isNaN(cr)){
           return Double.NaN ;
        }*/
        CorrelationResult r=new DfCorrelationResult(cr,numOfInvalid);
        return r;
    }

    private static double getStandDeviation(DoubleArrayList d1) {
        double var1 = Descriptive.variance(d1.size() ,Descriptive.sum(d1),Descriptive.sumOfSquares(d1) );
        double s1=Descriptive.standardDeviation(var1);
        return s1;
    }

    public static DoubleArrayList[] getValidData(double[] d1,double[] d2){
          //int count=0;
          DoubleArrayList v1=new DoubleArrayList();
          DoubleArrayList v2=new DoubleArrayList();
          for (int i=0;i<d1.length ;i++){
              if (!Double.isNaN(d1[i])&&
                  !Double.isNaN(d2[i])) {
                  v1.add(d1[i]);
                  v2.add(d2[i]);
              }

          }
          v1.trimToSize();// 
          v2.trimToSize();
          return new DoubleArrayList[]{v1,v2};

      }

    /*******************************************************************************************************
     *                Service components
     *******************************************************************************************************/
    public void addService(String name,ServiceComponent o) {
        o.setDataModel(this.dataModel );
        this.components.put(name,o);
    }
    public void removeService(String name) {
        this.components.remove(name);
    }
    public Object getServiceComponent(String name){
        return components.get(name);
    }
    private void updateComponents(){
         Enumeration e=this.components.elements() ;
         while (e.hasMoreElements()) {
             ServiceComponent component = (ServiceComponent) e.nextElement();
             component.setDataModel(dataModel) ;
         }
    }

    public AppDataModel getDataModel() {
        return dataModel;
    }

    public void setDataModel(edu.psu.geovista.data.model.AppDataModel dataModel) {
        this.dataModel = dataModel;
        this.updateComponents() ;
    }
    /**
     *
     * @return   the number of record this service provide
     */
    public int getNumberOfRecord(){
          return this.dataModel.getRowCount() ;
    }
    /**
     *
     * @param id  id of record in current dataModel
     * @return
     */
    public String getObservationName(int id){
        return this.dataModel.getRowName(id);
    }
}
